/*
 * Copyright (C) 2001-2016 Food and Agriculture Organization of the
 * United Nations (FAO-UN), United Nations World Food Programme (WFP)
 * and United Nations Environment Programme (UNEP)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 *
 * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
 * Rome - Italy. email: geonetwork@osgeo.org
 */

package org.fao.geonet.repository;

import org.fao.geonet.domain.*;
import org.fao.geonet.utils.Log;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Tuple;
import javax.persistence.criteria.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Implementation for all {@link User} queries that cannot be automatically generated by
 * Spring-data.
 *
 * @author Jesse
 */
public class UserRepositoryImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager _entityManager;

    @Override
    public User findOne(final String userId) {
        return _entityManager.find(User.class, Integer.valueOf(userId));
    }

    @Override
    public User findOneByEmail(final String email) {
        CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);

        query.where(cb.isMember(email, root.get(User_.emailAddresses)));
        final List<User> resultList = _entityManager.createQuery(query).getResultList();
        if (resultList.isEmpty()) {
            return null;
        }
        if (resultList.size() > 1) {
            Log.error(Constants.DOMAIN_LOG_MODULE, "The database is inconsistent.  There are multiple users with the email address: " +
                email);
        }
        return resultList.get(0);
    }

    @Override
    public User findOneByEmailAndSecurityAuthTypeIsNullOrEmpty(final String email) {
        CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);

        final Path<String> authTypePath = root.get(User_.security).get(UserSecurity_.authType);
        query.where(cb.and(
            cb.isMember(email, root.get(User_.emailAddresses)),
            cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))));
        List<User> results = _entityManager.createQuery(query).getResultList();


        if (results.isEmpty()) {
            return null;
        } else {
            return results.get(0);
        }
    }

    @Override
    public User findOneByUsernameAndSecurityAuthTypeIsNullOrEmpty(final String username) {
        CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);

        final Path<String> authTypePath = root.get(User_.security).get(UserSecurity_.authType);
        final Path<String> usernamePath = root.get(User_.username);
        query.where(cb.and(cb.equal(usernamePath, username), cb.or(cb.isNull(authTypePath), cb.equal(cb.trim(authTypePath), ""))));
        List<User> results = _entityManager.createQuery(query).getResultList();


        if (results.isEmpty()) {
            return null;
        } else {
            return results.get(0);
        }
    }

    @Nonnull
    @Override
    public List<String> findDuplicatedUsernamesCaseInsensitive() {
        CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        CriteriaQuery<String> query = cb.createQuery(String.class);

        Root<User> userRoot = query.from(User.class);
        query = query.select(cb.lower(userRoot.get(User_.username)));
        query.groupBy(cb.lower(userRoot.get(User_.username)));
        query.having(cb.gt(cb.count(userRoot), 1));

        return _entityManager.createQuery(query).getResultList();
    }

    @Override
    @Nonnull
    public List<Pair<Integer, User>> findAllByGroupOwnerNameAndProfile(@Nonnull final Collection<Integer> metadataIds,
                                                                       @Nullable final Profile profile, @Nullable final Sort sort) {
        CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        CriteriaQuery<Tuple> query = cb.createQuery(Tuple.class);

        Root<User> userRoot = query.from(User.class);
        Root<Metadata> metadataRoot = query.from(Metadata.class);
        Root<UserGroup> userGroupRoot = query.from(UserGroup.class);

        query.multiselect(metadataRoot.get(Metadata_.id), userRoot);

        Predicate metadataPredicate = metadataRoot.get(Metadata_.id).in(metadataIds);
        Predicate ownerPredicate = cb.equal(metadataRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.groupOwner),
            userGroupRoot.get(UserGroup_.id).get(UserGroupId_.groupId));
        Predicate userToGroupPredicate = cb.equal(userGroupRoot.get(UserGroup_.id).get(UserGroupId_.userId), userRoot.get(User_.id));

        Predicate basePredicate = cb.and(metadataPredicate, ownerPredicate, userToGroupPredicate);
        if (profile != null) {
            Expression<Boolean> profilePredicate = cb.equal(userGroupRoot.get(UserGroup_.id).get(UserGroupId_.profile), profile);
            query.where(cb.and(basePredicate, profilePredicate));
        } else {
            query.where(basePredicate);
        }
        if (sort != null) {
            List<Order> orders = SortUtils.sortToJpaOrders(cb, sort, userGroupRoot, metadataRoot, userRoot);
            query.orderBy(orders);
        }
        query.distinct(true);

        List<Pair<Integer, User>> results = new ArrayList<Pair<Integer, User>>();

        for (Tuple result : _entityManager.createQuery(query).getResultList()) {
            Integer mdId = (Integer) result.get(0);
            User user = (User) result.get(1);
            results.add(Pair.read(mdId, user));
        }
        return results;
    }

    @Nonnull
    @Override
    public List<User> findAllUsersThatOwnMetadata() {
        final CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        final CriteriaQuery<User> query = cb.createQuery(User.class);

        final Root<Metadata> metadataRoot = query.from(Metadata.class);
        final Root<User> userRoot = query.from(User.class);

        query.select(userRoot);

        final Path<Integer> ownerPath = metadataRoot.get(Metadata_.sourceInfo).get(MetadataSourceInfo_.owner);
        Expression<Boolean> ownerExpression = cb.equal(ownerPath, userRoot.get(User_.id));
        query.where(ownerExpression);
        query.distinct(true);

        return _entityManager.createQuery(query).getResultList();
    }

    @Nonnull
    @Override
    public List<User> findAllUsersInUserGroups(@Nonnull final Specification<UserGroup> userGroupSpec) {
        final CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
        final CriteriaQuery<User> query = cb.createQuery(User.class);

        final Root<UserGroup> userGroupRoot = query.from(UserGroup.class);
        final Root<User> userRoot = query.from(User.class);

        query.select(userRoot);

        final Path<Integer> ownerPath = userGroupRoot.get(UserGroup_.id).get(UserGroupId_.userId);
        Expression<Boolean> ownerExpression = cb.equal(ownerPath, userRoot.get(User_.id));
        query.where(cb.and(ownerExpression, userGroupSpec.toPredicate(userGroupRoot, query, cb)));
        query.distinct(true);

        return _entityManager.createQuery(query).getResultList();
    }

}
